package connectors;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;

import org.apache.commons.math3.geometry.euclidean.threed.Line;
import org.apache.commons.math3.geometry.euclidean.threed.Segment;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;

import utils.SynapseRandomizer;
import utils.math_utils.Coordinate;
import utils.math_utils.MathUtils;
import utils.math_utils.RandStream;

import components.AbstractNeuron;
import components.Synapse;

public class AxonOutgrowth extends Connect {
	
	private HashMap<Vector3D, AbstractNeuron> neuronMap;
	private double connectProb;
	private double maxDist;
	private double axonLength;
	
	//TODO: Default Synapse Randomizers
	public AxonOutgrowth(AbstractNeuron [] neurons, double axonLength,
			double maxDist, double connectProb, SynapseRandomizer eeR,
			SynapseRandomizer eiR, SynapseRandomizer ieR,
			SynapseRandomizer iiR){
		super(eeR, eiR, ieR, iiR);
		this.axonLength = axonLength;
		this.maxDist = maxDist;
		this.connectProb = connectProb;
		neuronMap = new HashMap<Vector3D, AbstractNeuron>();
		
		for(int i = 0, n = neurons.length; i < n; i++) {
			double [] coordinate = new double [3];
			coordinate[0] = neurons[i].getCoordinates().getX();
			coordinate[1] = neurons[i].getCoordinates().getY();
			coordinate[2] = neurons[i].getCoordinates().getZ();
			
			Vector3D pt = new Vector3D(coordinate);
			
			neuronMap.put(pt,neurons[i]);
			
		}	
		
	}
	
	public Synapse [][] connect (){
		Synapse [][] synapseMat =
				new Synapse[neuronMap.size()][neuronMap.size()];
		for(Vector3D pt : neuronMap.keySet()) {
			AbstractNeuron src = neuronMap.get(pt);
			boolean se = src.isExcitatory();
			boolean te;
			Segment axon = createRandomAxon(pt, axonLength);
			ArrayList<AbstractNeuron> candidateNeurons =
					(ArrayList<AbstractNeuron>) findNeuronsInRange(axon);
			for(AbstractNeuron targ : candidateNeurons) {
				if(Math.random() < connectProb) {
					Synapse s = new Synapse(src, targ);
					double dist = MathUtils.euclideanDist(src.getCoordinates(),
							targ.getCoordinates()) * 60;
					
					s.setDelay(dist/1000 + 0.8);
					
					if(src.isExcitatory() && targ.isExcitatory()) {
						getEeSR().randomizeSynapse(s);

						s.setStrength(5*RandStream.nextGaussian(30, 15, 1, 100, true));				
			
						s.setDecay(3);
						//s.setDelay(1.5);
					} else if (src.isExcitatory() && !targ.isExcitatory()) {
						getEiSR().randomizeSynapse(s);

							s.setStrength(5*RandStream.nextGaussian(60, 30, 1, 200, true));
			
						s.setDecay(3);
						//s.setDelay(0.8);
					} else if (!src.isExcitatory() && targ.isExcitatory()) {
						getIeSR().randomizeSynapse(s);
						s.setStrength(5*RandStream.nextGaussian(19, 8, 1, 38, true));
						s.setDecay(6);
						//s.setDelay(0.8);
					} else {			
						getIiSR().randomizeSynapse(s);
						s.setStrength(5*RandStream.nextGaussian(19, 8, 1, 38, true));
						s.setDecay(6);
						//s.setDelay(0.8);
					}
					//double dist = MathUtils.euclideanDist(src.getCoordinates(), targ.getCoordinates());
					//s.setDelay(0.8 + Math.log(dist)/2);
					src.getFanOut().add(s);
					targ.getFanIn().add(s);
				}
			}
			
		}
		
		return synapseMat;
	}
	
	private Segment createRandomAxon(Vector3D origin, double mag) {
		double theta = Math.random() * 2 * Math.PI;
		double phi = Math.random() * 2 * Math.PI;
		double [] v = new double[3];

		v[0] = mag * Math.sin(phi) * Math.cos(theta) + origin.getX();	//x
		v[1] = mag * Math.sin(phi) * Math.sin(theta) + origin.getY();	//y
		v[2] = mag * Math.cos(theta) + origin.getZ();				 	//z	
		Vector3D p = new Vector3D(v);
		Line l = new Line(origin, p);
		return new Segment(origin, p, l);
	}
	
	private List<AbstractNeuron> findNeuronsInRange(Segment axon) {
		Vector3D end = axon.getEnd();
		Vector3D start = axon.getStart();
		HashSet<Vector3D> possible = (HashSet<Vector3D>)neuronMap.keySet();

		Vector3D nEnd = end.subtract(start);
		
		for(Vector3D v : neuronMap.keySet()){
			if(v.equals(start)) {
				possible.remove(v);
				continue;
			}
			Vector3D pt = v.subtract(start);
			double d = pt.dotProduct(nEnd);
			if(d < 0) {
				possible.remove(v);
			}
		}
		ArrayList<AbstractNeuron> neuronList =
				new ArrayList<AbstractNeuron>();
		for(Vector3D neuron : possible) {
			double dist = findMinDistance(axon, neuron);
			if(dist <= maxDist){
				neuronList.add(neuronMap.get(neuron));
			}		
		}				
		return neuronList;
	}
	
	private double findMinDistance(Segment axon, Vector3D pt) {
		Vector3D end = axon.getEnd();
		Vector3D start = axon.getStart();
		
		double dist = Vector3D.crossProduct(pt.subtract(start),
				pt.subtract(end)).getNorm();
		dist = dist/(end.subtract(start).getNorm());
		return dist;	
	}
	
	
	private static void randomPlacement(short [] dims, AbstractNeuron [] neurons) {
		short X = dims[0];
		short Y = dims[1];
		short Z = dims[2];
		
		if(X * Y * Z < neurons.length){
			throw new IllegalArgumentException("Specified dimensions cannot" +
					" support the requested number of neurons");
		}
		
		short[][] availableCoordinates = new short[X * Y * Z][3];
		int row = 0;
		for(short x = 0; x < X; x++){
			for(short y = 0; y < Y; y++){
				for(short z = 0; z < Z; z++){
					short [] coordinate = new short[3];
					coordinate[0] = x;
					coordinate[1] = y;
					coordinate[2] = z;
					availableCoordinates[row] = Arrays.copyOf(coordinate, 3);
				}
			}
		}
		
		Random randi = new Random();
		short [] holder;
		int j;
		
		for(int i = 0, n = neurons.length; i < n; i++) {
			holder = availableCoordinates[i];
			j = randi.nextInt(n - i) + i;
			availableCoordinates[i] = availableCoordinates[j];
			availableCoordinates[j] = holder;
			neurons[i].setCoordinates(new Coordinate(availableCoordinates[i]));
		}		
		
	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub

	}

}
